// This source code file (and the respective header file) have been copied and
// adapted from the MythTV project source file:
//
// http://svn.mythtv.org/trac/browser/trunk/mythtv/libs/libmythui/AppleRemote.cpp

#include "appleremote.h"

#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <CoreFoundation/CoreFoundation.h>
#include <QDebug>
#include <QKeySequence>
#include <QApplication>
#include <QKeyEvent>

#define REMOTE_SWITCH_COOKIE 19

AppleRemote::AppleRemote(QObject* receiver, bool exclusive):
        receiver(receiver),
        openInExclusiveMode(exclusive),
        hidDeviceInterface(0),
        queue(0),
        remoteId(0)
{
    initCookieMap();
}

AppleRemote::~AppleRemote()
{
    stopListening();
}

bool AppleRemote::isListeningToRemote()
{
    return (hidDeviceInterface && !cookies.empty() && queue);
}

void AppleRemote::startListening()
{
    if (queue)   // already listening
        return;
 
    io_object_t hidDevice = findAppleRemoteDevice();
    if (!hidDevice ||
        !createDeviceInterface(hidDevice) ||
        !initCookies() ||
        !openDevice())
        stopListening();

    IOObjectRelease(hidDevice);
}

void AppleRemote::stopListening()
{
    if (queue) {
        (*queue)->stop(queue);
        (*queue)->dispose(queue);
        (*queue)->Release(queue);
        queue = 0;
    }

    if (!cookies.empty())
        cookies.clear();

    if (hidDeviceInterface) {
        (*hidDeviceInterface)->close(hidDeviceInterface);
        (*hidDeviceInterface)->Release(hidDeviceInterface);
        hidDeviceInterface = 0;
    }
}

void AppleRemote::run()
{
    CFRunLoopRun();
    exec();  // prevent QThread exiting, by entering its run loop
    CFRunLoopStop(CFRunLoopGetCurrent());
}

void AppleRemote::initCookieMap()
{
    // 10.4 sequences:
    cookieToButtonMapping["14_12_11_6_5_"]        = VolumePlus;
    cookieToButtonMapping["14_13_11_6_5_"]        = VolumeMinus;
    cookieToButtonMapping["14_7_6_5_14_7_6_5_"]   = Menu;
    cookieToButtonMapping["14_8_6_5_14_8_6_5_"]   = Play;
    cookieToButtonMapping["14_9_6_5_14_9_6_5_"]   = Right;
    cookieToButtonMapping["14_10_6_5_14_10_6_5_"] = Left;
    cookieToButtonMapping["14_6_5_4_2_"]          = RightHold;
    cookieToButtonMapping["14_6_5_3_2_"]          = LeftHold;
    cookieToButtonMapping["14_6_5_14_6_5_"]       = MenuHold;
    cookieToButtonMapping["18_14_6_5_18_14_6_5_"] = PlaySleep;
    cookieToButtonMapping["19_"]                  = ControlSwitched;

    // 10.5 sequences:
    cookieToButtonMapping["31_29_28_18_"]         = VolumePlus;
    cookieToButtonMapping["31_30_28_18_"]         = VolumeMinus;
    cookieToButtonMapping["31_20_18_31_20_18_"]   = Menu;
    cookieToButtonMapping["31_21_18_31_21_18_"]   = Play;
    cookieToButtonMapping["31_22_18_31_22_18_"]   = Right;
    cookieToButtonMapping["31_23_18_31_23_18_"]   = Left;
    cookieToButtonMapping["31_18_4_2_"]           = RightHold;
    cookieToButtonMapping["31_18_3_2_"]           = LeftHold;
    cookieToButtonMapping["31_18_31_18_"]         = MenuHold;
    cookieToButtonMapping["35_31_18_35_31_18_"]   = PlaySleep;
    cookieToButtonMapping["39_"]                  = ControlSwitched;
}

io_object_t AppleRemote::findAppleRemoteDevice()
{
    io_iterator_t hidObjectIterator = 0;
    io_object_t hidDevice = 0;

    CFMutableDictionaryRef hidMatchDictionary = IOServiceMatching("AppleIRController");

    // check for matching devices
    IOReturn ioReturnValue =
            IOServiceGetMatchingServices(kIOMasterPortDefault,
                                         hidMatchDictionary,
                                         &hidObjectIterator);

    if ((ioReturnValue == kIOReturnSuccess) && hidObjectIterator)
        hidDevice = IOIteratorNext(hidObjectIterator);

    // IOServiceGetMatchingServices consumes a reference to the dictionary,
    // so we don't need to release the dictionary ref.
    hidMatchDictionary = 0;
    return hidDevice;
}

bool AppleRemote::initCookies()
{
    IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface;

    CFArrayRef elements;
    if ((*handle)->copyMatchingElements(handle, NULL, &elements) != kIOReturnSuccess)
        return false;

    for (CFIndex i = 0; i < CFArrayGetCount(elements); ++i) {
        CFDictionaryRef element = (CFDictionaryRef)CFArrayGetValueAtIndex(elements, i);
        CFTypeRef object = CFDictionaryGetValue(element, CFSTR(kIOHIDElementCookieKey));
        if (object && (CFGetTypeID(object) == CFNumberGetTypeID())) {
            long number;
            if (CFNumberGetValue((CFNumberRef)object, kCFNumberLongType, &number))
                cookies.append((IOHIDElementCookie) number);
        }
    }
    return true;
}

bool AppleRemote::createDeviceInterface(io_object_t hidDevice)
{
    IOCFPlugInInterface** plugInInterface = NULL;
    SInt32 score = 0;
    IOReturn ioReturnValue =
            IOCreatePlugInInterfaceForService(hidDevice,
                                              kIOHIDDeviceUserClientTypeID,
                                              kIOCFPlugInInterfaceID,
                                              &plugInInterface, &score);

    if (ioReturnValue == kIOReturnSuccess) {
        HRESULT plugInResult =
                (*plugInInterface)->QueryInterface(plugInInterface,
                                                   CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID),
                                                   (LPVOID*) (&hidDeviceInterface));
        if (plugInResult != S_OK)
            qDebug() << "createDeviceInterface() failed";
        if (plugInInterface)
            (*plugInInterface)->Release(plugInInterface);
    }
    return hidDeviceInterface != 0;
}

bool AppleRemote::openDevice()
{
    IOHIDOptionsType openMode =
            openInExclusiveMode ? kIOHIDOptionsTypeSeizeDevice : kIOHIDOptionsTypeNone;
    IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode);

    if (ioReturnValue != KERN_SUCCESS) {
        qDebug() << "openDevice() failed";
        return false;
    }

    queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface);
    if (!queue) {
        qDebug() << "openDevice() - error allocating queue";
        return false;
    }

    HRESULT result = (*queue)->create(queue, 0, 12);
    if (result != S_OK || !queue)
        qDebug() << "openDevice() - error creating queue";

    foreach (IOHIDElementCookie c, cookies)
        (*queue)->addElement(queue, c, 0);

    CFRunLoopSourceRef eventSource;
    ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource);
    if (ioReturnValue != KERN_SUCCESS) {
        qDebug() << "openDevice() - failed to create async event source";
        return false;
    }

    ioReturnValue = (*queue)->setEventCallout(queue, queueCallbackFunction, this, NULL);
    if (ioReturnValue != KERN_SUCCESS) {
        qDebug() <<"_openDevice() - error registering callback";
        return false;
    }

    CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
    (*queue)->start(queue);
    return true;
}

void AppleRemote::queueCallbackFunction(void* target, IOReturn result, void*, void*)
{
    AppleRemote* self = static_cast<AppleRemote*>(target);
    AbsoluteTime zeroTime = { 0, 0 };
    int sumOfValues = 0;
    QByteArray cookieString;

    while (result == kIOReturnSuccess) {
        IOHIDEventStruct event;
        result = (*self->queue)->getNextEvent(self->queue, &event, zeroTime, 0);
        if (result != kIOReturnSuccess)
            break;

        const QString token = QString("%0_").arg(int(event.elementCookie));
        if (int(event.elementCookie) == REMOTE_SWITCH_COOKIE) {
            self->remoteId = event.value;
            self->handleEventWithCookieString(token, 0);
        }
        else {
            sumOfValues += event.value;
            cookieString.append(token);
        }
    }

    self->handleEventWithCookieString(cookieString, sumOfValues);
}

void AppleRemote::handleEventWithCookieString(const QString& cookieString, int sumOfValues)
{
    QMap<QString, Event>::Iterator mapping = cookieToButtonMapping.find(cookieString);
    if (!receiver || mapping == cookieToButtonMapping.end() && receiver)
        return;

    const char* code = 0;
    bool separateRelease = false;
    bool pressedDown = (sumOfValues>0);

    switch (mapping.value()) {
    case AppleRemote::VolumePlus:
        code = "Up";
        separateRelease = true;
        break;
    case AppleRemote::VolumeMinus:
        code = "Down";
        separateRelease = true;
        break;
    case AppleRemote::Menu:
        code = "Esc";
        break;
    case AppleRemote::Play:
        code = "Enter";
        break;
    case AppleRemote::Right:
        code = "Right";
        break;
    case AppleRemote::Left:
        code = "Left";
        break;
    case AppleRemote::RightHold:
        code = "End";
        separateRelease = true;
        break;
    case AppleRemote::LeftHold:
        code = "Home";
        separateRelease = true;
        break;
    case AppleRemote::MenuHold:
        code = "M";
        break;
    case AppleRemote::PlaySleep:
        code = "P";
        break;
    default:
    case AppleRemote::ControlSwitched:
        return;
    }

    // send key events for each code in the key sequence
    QKeySequence seq(code);
    for (unsigned int i = 0; i < seq.count(); ++i) {
        if (pressedDown)
            QApplication::postEvent(receiver,
                    new QKeyEvent(QEvent::KeyPress, seq[i], Qt::NoModifier, code));
        if (!separateRelease || !pressedDown)
            QApplication::postEvent(receiver,
                    new QKeyEvent(QEvent::KeyRelease, seq[i], Qt::NoModifier, code));
    }
}
